package com.dbflow5.config;

import com.dbflow5.StringUtils;
import com.dbflow5.adapter.ModelAdapter;
import com.dbflow5.adapter.ModelViewAdapter;
import com.dbflow5.adapter.RetrievalAdapter;
import com.dbflow5.annotation.Table;
import com.dbflow5.converter.TypeConverters;
import com.dbflow5.database.DatabaseWrapper;
import com.dbflow5.migration.Migration;
import com.dbflow5.runtime.ModelNotifier;
import com.dbflow5.runtime.TableNotifierRegister;
import com.dbflow5.structure.BaseModel;
import com.dbflow5.structure.BaseModelView;
import com.dbflow5.structure.BaseQueryModel;
import com.dbflow5.structure.InvalidDBConfiguration;
import com.dbflow5.structure.Model;
import ohos.aafwk.ability.DataAbilityHelper;
import ohos.app.Context;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;

/**
 * Description: The main entry point into the generated database code. It uses reflection to look up
 * and construct the generated database holder class used in defining the structure for all databases
 * used in this application.
 */
public class FlowManager {
    private static final String DEFAULT_DATABASE_HOLDER_NAME = "GeneratedDatabaseHolder";
    //@Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
    private static final String DEFAULT_DATABASE_HOLDER_PACKAGE_NAME = FlowManager.class.getPackage().getName();
    private static final String DEFAULT_DATABASE_HOLDER_CLASSNAME = DEFAULT_DATABASE_HOLDER_PACKAGE_NAME+"."+DEFAULT_DATABASE_HOLDER_NAME;

    private static FlowConfig config = null;

    private static GlobalDatabaseHolder globalDatabaseHolder = new GlobalDatabaseHolder();

    private static final Set<Class<DatabaseHolder>> loadedModules = new HashSet<>();


    /**
     * Override for testing
     */
    //@set:TestOnly
    public static DataAbilityHelper globalContentResolver = null;

    /**
     * Will throw an exception if this class is not initialized yet in [.init]
     *
     * @return The shared context.
     */
    public static Context getContext(){
        if(config != null){
            return config.context;
        }
        throw new IllegalStateException("You must provide a valid FlowConfig instance." +
                " We recommend calling init() in your application class.");
    }

    public static DataAbilityHelper contentResolver(){
        if(globalContentResolver == null){
            return DataAbilityHelper.creator(getContext());
        }
        return globalContentResolver;
    }

    private static class GlobalDatabaseHolder extends DatabaseHolder {

        boolean isInitialized = false;

        void add(DatabaseHolder holder) {
            databaseDefinitionMap.putAll(holder.databaseDefinitionMap);
            databaseNameMap.putAll(holder.databaseNameMap);
            typeConverters.putAll(holder.typeConverters);
            databaseClassLookupMap.putAll(holder.databaseClassLookupMap);
            isInitialized = true;
        }
    }

    /**
     * Returns the table name for the specific model class
     *
     * @param table The class that implements [Model]
     * @return The table name, which can be different than the [Model] class name
     */
    public static String getTableName(Class<?> table) {
        String name = getModelAdapterOrNull(table).getName();
        if(name == null){
            name = getModelViewAdapterOrNull(table).getName();
        }
        if(name == null){
            throw new IllegalArgumentException("Cannot find "+"ModelAdapter/ModelViewAdapter/VirtualAdapter"+" for "+table+". Ensure the class is annotated with proper annotation.");
        }
        return name;
    }

    /**
     * getTableClassForName
     *
     * @param databaseName The name of the database. Will throw an exception if the databaseForTable doesn't exist.
     * @param tableName    The name of the table in the DB.
     * @return The associated table class for the specified name.
     */
    public static Class<?> getTableClassForName(String databaseName, String tableName) {
        DBFlowDatabase databaseDefinition = getDatabase(databaseName);
        Class<?> clazz = databaseDefinition.getModelClassForName(tableName);
        if(clazz == null){
            clazz = databaseDefinition.getModelClassForName(StringUtils.quote(tableName));
        }
        if(clazz == null){
            throw new IllegalArgumentException("The specified table "+tableName+" was not found." +
                    " Did you forget to add the @Table annotation and point it to "+databaseName+"?");
        }
        return clazz;
    }

    /**
     * getTableClassForName
     *
     * @param databaseClass The class of the database. Will throw an exception if the databaseForTable doesn't exist.
     * @param tableName     The name of the table in the DB.
     * @return The associated table class for the specified name.
     */
    public static Class<?> getTableClassForName(Class<? extends DBFlowDatabase> databaseClass, String tableName) {
        DBFlowDatabase databaseDefinition = getDatabase(databaseClass);
        Class<?> clazz = databaseDefinition.getModelClassForName(tableName);
        if(clazz == null){
            clazz = databaseDefinition.getModelClassForName(StringUtils.quote(tableName));
        }
        if(clazz == null){
            throw new IllegalArgumentException("The specified table "+tableName+" was not found." +
                    " Did you forget to add the @Table annotation and point it to "+databaseClass+"?");
        }
        return clazz;
    }

    /**
     * getDatabaseForTable
     *
     * @param table The table to lookup the database for.
     * @return the corresponding [DBFlowDatabase] for the specified model
     */
    public static DBFlowDatabase getDatabaseForTable(Class<?> table) {
        checkDatabaseHolder();
        DBFlowDatabase dbFlowDatabase = globalDatabaseHolder.getDatabaseForTable(table);
        if(dbFlowDatabase == null){
            throw new InvalidDBConfiguration("Model object: "+table+" is not registered with a Database." +
                    " Did you forget an annotation?");
        }
        return dbFlowDatabase;
    }

    public static  <T extends DBFlowDatabase> T getDatabase(Class<T> databaseClass) {
        checkDatabaseHolder();

        T t = (T) globalDatabaseHolder.getDatabase(databaseClass);
        if(t == null){
            throw new InvalidDBConfiguration("Database: "+databaseClass.getName()+" is not a registered Database. " +
                    "Did you forget the @Database annotation?");
        }
        return t;
    }

    public static String getDatabaseName(Class<? extends DBFlowDatabase> database) {
        return getDatabase(database).getDatabaseName();
    }

    /**
     * getDatabase
     *
     * @param databaseName The name of the database. Will throw an exception if the databaseForTable doesn't exist.
     * @return the [DBFlowDatabase] for the specified database
     */
    public static DBFlowDatabase getDatabase(String databaseName) {
        checkDatabaseHolder();
        DBFlowDatabase dbFlowDatabase = globalDatabaseHolder.getDatabase(databaseName);
        if(dbFlowDatabase == null){
            throw new InvalidDBConfiguration("The specified database "+databaseName+" was not found. " +
                    "Did you forget the @Database annotation?");
        }
        return dbFlowDatabase;
    }

    @Deprecated
    public static DatabaseWrapper getWritableDatabaseForTable(Class<?> table) {
        return getDatabaseForTable(table).getWritableDatabase();
    }

    @Deprecated
    public static DatabaseWrapper getWritableDatabase(String databaseName) {
        return getDatabase(databaseName).getWritableDatabase();
    }

    @Deprecated
    public static DatabaseWrapper getWritableDatabase(Class<DBFlowDatabase> databaseClass){
        return getDatabase(databaseClass).getWritableDatabase();
    }

    /**
     * Loading the module Database holder via reflection.
     * It is assumed FlowManager.init() is called by the application that uses the
     * module database. This method should only be called if you need to load databases
     * that are part of a module. Building once will give you the ability to add the class.
     *
     * @param generatedClassName generatedClassName
     */
    public static void initModule(Class<DatabaseHolder> generatedClassName) {
        loadDatabaseHolder(generatedClassName);
    }

    public static FlowConfig getConfig() {
        if(config != null){
            return config;
        }
        throw new IllegalStateException("Configuration is not initialized. " +
                "Please call init(FlowConfig) in your application class.");
    }

    /**
     * loadDatabaseHolder
     *
     * @param holderClass The database holder, creating if necessary using reflection.
     */
    private static void loadDatabaseHolder(Class<DatabaseHolder> holderClass) {
        if (loadedModules.contains(holderClass)) {
            return;
        }

        try {
            // Load the database holder, and add it to the global collection.
            DatabaseHolder dbHolder = holderClass.newInstance();
            if (dbHolder != null) {
                globalDatabaseHolder.add(dbHolder);

                // Cache the holder for future reference.
                loadedModules.add(holderClass);
            }
        } catch (Exception e) {
            throw new ModuleNotFoundException("Cannot load " + holderClass, e);
        }

    }

    /**
     * Resets all databases and associated files.
     */
    public synchronized static void reset() {
        Collection<DBFlowDatabase> flowDatabases = globalDatabaseHolder.databaseClassLookupMap.values();
        for(DBFlowDatabase database : flowDatabases){
            database.reset(database.databaseConfig);
        }
        globalDatabaseHolder.reset();
        loadedModules.clear();
    }

    /**
     * Close all DB files and resets [FlowConfig] and the [GlobalDatabaseHolder]. Brings
     * DBFlow back to initial application state.
     */
    public synchronized static void close() {
        Collection<DBFlowDatabase> flowDatabases = globalDatabaseHolder.databaseClassLookupMap.values();
        for(DBFlowDatabase database : flowDatabases){
            database.close();
        }

        config = null;
        globalDatabaseHolder = new GlobalDatabaseHolder();
        loadedModules.clear();
    }

    /**
     * Helper method to simplify the [.init]. Use [.init] to provide
     * more customization.
     *
     * @param context - should be application context, but not necessary as we retrieve it anyways.
     * @param config config
     */
    public static void init(Context context, Function<FlowConfig.Builder, Void> config) {
        init(FlowConfig.flowConfig(context, config));
    }

    /**
     * Initializes DBFlow, loading the main application Database holder via reflection one time only.
     * This will trigger all creations, updates, and instantiation for each database defined.
     *
     * @param flowConfig The configuration instance that will help shape how DBFlow gets constructed.
     */
    public static void init(FlowConfig flowConfig) {
        if(config == null){
            config = flowConfig;
        }else {
            config = config.merge(flowConfig);
        }

        try {
            Class<DatabaseHolder> defaultHolderClass = (Class<DatabaseHolder>)Class.forName(DEFAULT_DATABASE_HOLDER_CLASSNAME);
            loadDatabaseHolder(defaultHolderClass);
        } catch (ModuleNotFoundException e) {
            // Ignore this exception since it means the application does not have its
            // own database. The initialization happens because the application is using
            // a module that has a database.
            FlowLog.log(FlowLog.Level.W, e.getMessage());
        } catch (ClassNotFoundException e) {
            // warning if a library uses DBFlow with module support but the app you're using doesn't support it.
            FlowLog.log(FlowLog.Level.W, "Could not find the default GeneratedDatabaseHolder");
        }

        for (Class<DatabaseHolder> clazz : flowConfig.databaseHolders){
            loadDatabaseHolder(clazz);
        }

        if (flowConfig.openDatabasesOnInit) {
            for(DBFlowDatabase database : globalDatabaseHolder.databaseDefinitions()){
                database.getWritableDatabase();
            }
        }
    }

    /**
     * how the custom datatype is handled going into and out of the DB.
     *
     * @param objectClass A class with an associated type converter. May return null if not found.
     * @return The specific [TypeConverter] for the specified class. It defines
     */
    public static TypeConverters.TypeConverter<?, ?> getTypeConverterForClass(Class<?> objectClass) {
        checkDatabaseHolder();
        return globalDatabaseHolder.getTypeConverterForClass(objectClass);
    }

    /**
     * Release reference to context and [FlowConfig]
     */
    public synchronized static void destroy() {
        Collection<DBFlowDatabase> flowDatabases = globalDatabaseHolder.databaseClassLookupMap.values();
        for (DBFlowDatabase database : flowDatabases){
            database.destroy();
        }

        config = null;
        // Reset the global database holder.
        globalDatabaseHolder = new GlobalDatabaseHolder();
        loadedModules.clear();
    }

    /**
     * it checks both the [ModelViewAdapter] and [RetrievalAdapter].
     *
     * @param modelClass The class that implements [Model] to find an adapter for.
     * @param <T> t
     * @return retrievalAdapter
     */
    public static <T> RetrievalAdapter<T> getRetrievalAdapter(Class<T> modelClass) {
        RetrievalAdapter<T> retrievalAdapter = getModelAdapterOrNull(modelClass);
        if (retrievalAdapter == null) {
            retrievalAdapter = getModelViewAdapterOrNull(modelClass);
            if(retrievalAdapter == null){
                retrievalAdapter = getQueryModelAdapterOrNull(modelClass);
            }
        }
        if(retrievalAdapter != null){
            return retrievalAdapter;
        }
        throw new IllegalArgumentException("Cannot find "+"RetrievalAdapter"+" for "+modelClass+". Ensure the class is annotated with proper annotation.");
    }

    /**
     * interactions with the database. This method is meant for internal usage only.
     * We strongly prefer you use the built-in methods associated with [Model] and [BaseModel].
     *
     * @param modelClass The class of the table
     * @param <T> t
     * @return modelAdapter
     */
    public static <T> ModelAdapter<T> getModelAdapter(Class<T> modelClass){
        ModelAdapter<T> modelAdapter = getModelAdapterOrNull(modelClass);
        if(modelAdapter != null){
            return modelAdapter;
        }
        throw new IllegalArgumentException("Cannot find "+"ModelAdapter"+" for "+modelClass+". Ensure the class is annotated with proper annotation.");
    }

    /**
     * Returns the model view adapter for a SQLite VIEW. These are only created with the [com.dbflow5.annotation.ModelView] annotation.
     *
     * @param modelViewClass The class of the VIEW
     * @return The model view adapter for the specified model view.
     */
    public static <T> ModelViewAdapter<T> getModelViewAdapter(Class<T> modelViewClass) {
        ModelViewAdapter<T> modelViewAdapter = getModelViewAdapterOrNull(modelViewClass);
        if(modelViewAdapter != null){
            return modelViewAdapter;
        }
        throw new IllegalArgumentException("Cannot find "+"ModelViewAdapter"+" for "+modelViewClass+". Ensure the class is annotated with proper annotation.");
    }

    /**
     * Returns the query model adapter for the model class. These are only created with the [T] annotation.
     *
     * @param queryModelClass The class of the query
     * @return The query model adapter for the specified model cursor.
     */
    public static <T> RetrievalAdapter<T> getQueryModelAdapter(Class<T> queryModelClass){
        RetrievalAdapter<T> retrievalAdapter = getQueryModelAdapterOrNull(queryModelClass);
        if(retrievalAdapter != null){
            return retrievalAdapter;
        }
        throw new IllegalArgumentException("Cannot find "+"RetrievalAdapter"+" for "+queryModelClass+". Ensure the class is annotated with proper annotation.");
    }

    public static ModelNotifier getModelNotifierForTable(Class<?> table) {
        return getDatabaseForTable(table).getModelNotifier();
    }

    public static TableNotifierRegister newRegisterForTable(Class<?> table) {
        return getModelNotifierForTable(table).newRegister();
    }

    private static <T> ModelAdapter<T> getModelAdapterOrNull(Class<T> modelClass) {
        return getDatabaseForTable(modelClass).getModelAdapterForTable(modelClass);
    }

    private static  <T> ModelViewAdapter<T> getModelViewAdapterOrNull(Class<T> modelClass) {
        return getDatabaseForTable(modelClass).getModelViewAdapterForTable(modelClass);
    }

    private static <T> RetrievalAdapter<T> getQueryModelAdapterOrNull(Class<T> modelClass) {
        return getDatabaseForTable(modelClass).getQueryModelAdapterForQueryClass(modelClass);
    }

    /**
     * Checks a standard database helper for integrity using quick_check(1).
     *
     * @param databaseName The name of the database to check. Will thrown an exception if it does not exist.
     * @return true if it's integrity is OK.
     */
    public static boolean isDatabaseIntegrityOk(String databaseName){
        return getDatabase(databaseName).openHelper().isDatabaseIntegrityOk();
    }

    /**
     * throwCannotFindAdapter
     *
     * @param type type
     * @param clazz clazz
     */
    private static void throwCannotFindAdapter(String type, Class<?> clazz) {
        throw new IllegalArgumentException("Cannot find "+type+" for "+clazz+". Ensure the class is annotated with proper annotation.");
    }

    private static void checkDatabaseHolder() {
        if (!globalDatabaseHolder.isInitialized) {
            throw new IllegalStateException("The global databaseForTable holder is not initialized. " +
                "Ensure you call FlowManager.init() before accessing the databaseForTable.");
        }
    }

    /**
     * Exception thrown when a database holder cannot load the databaseForTable holder
     * for a module.
     */
    static class ModuleNotFoundException extends RuntimeException{
        ModuleNotFoundException(String detailMessage, Throwable throwable){
            super(detailMessage, throwable);
        }
    }

    /**
     * Easily get access to its [DBFlowDatabase] directly.
     *
     * @param clazz clazz
     * @param f f
     * @param <T> t
     * @return r
     */
    public static <T extends DBFlowDatabase> T database(Class<T> clazz, Function<T, Void> f){
        T t = FlowManager.getDatabase(clazz);
        f.apply(t);
        return t;
    }

    /**
     * Easily get access to its [DBFlowDatabase] directly.
     *
     * @param clazz clazz
     * @param f f
     * @param <T> t
     * @return database
     */
    public static <T> DBFlowDatabase databaseForTable(Class<T> clazz, Function<DBFlowDatabase, Void> f){
        DBFlowDatabase database = FlowManager.getDatabaseForTable(clazz);
        f.apply(database);
        return database;
    }


    /**
     * Easily get its table name.
     *
     * @param clazz clazz
     * @param <T> t
     * @return FlowManager.getTableName(clazz)
     */
    public static <T> String tableName(Class<T> clazz){
        return FlowManager.getTableName(clazz);
    }

    /**
     * Easily get its [ModelAdapter].
     *
     * @param clazz clazz
     * @param <T> t
     * @return FlowManager.getModelAdapter(clazz)
     */
    public static <T> ModelAdapter<T> modelAdapter(Class<T> clazz){
        return FlowManager.getModelAdapter(clazz);
    }

    /**
     * queryModelAdapter
     *
     * @param clazz clazz
     * @param <T> t
     * @return FlowManager.getQueryModelAdapter(clazz)
     */
    @Deprecated
    public static <T> RetrievalAdapter<T> queryModelAdapter(Class<T> clazz){
        return FlowManager.getQueryModelAdapter(clazz);
    }

    /**
     * Easily get its [RetrievalAdapter].
     *
     * @param clazz clazz
     * @param <T> t
     * @return  FlowManager.getQueryModelAdapter(clazz)
     */
    public static <T> RetrievalAdapter<T> retrievalAdapter(Class<T> clazz){
        return FlowManager.getQueryModelAdapter(clazz);
    }

    /**
     * Easily get its [ModelViewAdapter]
     *
     * @param clazz clazz
     * @param <T> t
     * @return  FlowManager.getModelViewAdapter(clazz)
     */
    public static <T> ModelViewAdapter<T> modelViewAdapter(Class<T> clazz) {
        return FlowManager.getModelViewAdapter(clazz);
    }
}

